package com.googlecode.vmock;

import com.googlecode.vmock.utils.Validate;

/**
 * Builder used to create instances of {@link ExpectedCallResult} bound to a given recorded call.
 * 
 * @author marcin.jagus
 */
public final class MockBuilder {

    /**
     * Defines a made up number of calls a mocked result should be returned that should be interpreted as "any times".
     */
    static int ANY_TIMES = -1;

    /**
     * Recorded call.
     */
    private RecordedCall call;

    /**
     * Result that (most likely) will be built by this builder.
     */
    private ExpectedCallResult expectedResult;

    /**
     * Creates new builder for a given call.
     * 
     * @param call Given call. Cannot be <code>null</code>.
     */
    MockBuilder(RecordedCall call) {
        Validate.notNull(call, "call == null");

        this.call = call;
        this.expectedResult = null;
        MockController.getInstance().registerRecordedCall(this.call);
    }

    /**
     * Sets the expected result to be a given value.
     * 
     * @param returnValue A given value which will be an expected result for this builder's recirded call. Can be <code>null</code>.
     */
    private void buildReturnResult(Object returnValue) {
        expectedResult = new ReturnCallResult(returnValue, call);
        MockController.getInstance().registerExpectedResult(call, expectedResult);
    }

    /**
     * Sets the expected result to be a value returned by a given {@link Answer} object.
     * 
     * @param answer Given answer object. Cannot be <code>null</code>.
     */
    private void buildAnswerResult(Answer<?> answer) {
        expectedResult = new AnswerCallResult(answer, call);
        MockController.getInstance().registerExpectedResult(call, expectedResult);
    }

    /**
     * Sets the expected result to be a given throwable object. Given throwable will be thrown whenever this builder's recorded call will be invoked.
     * 
     * @param throwable Given throwable. Cannot be <code>null</code>.
     */
    private void buildThrowResult(Throwable throwable) {
        expectedResult = new ThrowCallResult(throwable, call);
        MockController.getInstance().registerExpectedResult(call, expectedResult);
    }

    /**
     * Sub-builder that defines an expected result for this builder's call.
     * 
     * @author marcin.jagus
     * @param <T> Type that an expected result should have
     */
    public class ResultBuilder<T> extends CardinalityBuilder {

        /**
         * Creates this sub-builder.
         */
        protected ResultBuilder() {
        }

        /**
         * Sets the expected result of a call to a given value.
         * 
         * @param returnValue Given value. Can be <code>null</code>.
         * @return this builder
         */
        public CardinalityBuilder andReturn(T returnValue) {
            buildReturnResult(returnValue);
            return this;
        }

        /**
         * Binds a given {@link Answer} object to a call. {@link Answer#answer(Object...)} method of a given answer will be called every time an
         * expected result is requested.
         * 
         * @param answer Given answer object
         * @return this builder
         */
        public CardinalityBuilder andAnswer(Answer<T> answer) {
            buildAnswerResult(answer);
            return this;
        }

        /**
         * Sets the expected result to be a given throwable that will be thrown every time an expected result is requested.
         * 
         * @param throwable Given throwable
         * @return this builder
         */
        public CardinalityBuilder andThrow(Throwable throwable) {
            buildThrowResult(throwable);
            return this;
        }
    }

    /**
     * Sub-builder that defines the number of invocations that should return an expected result.
     * 
     * @author marcin-jagus
     */
    public class CardinalityBuilder {

        /**
         * Creates this sub-builder.
         */
        protected CardinalityBuilder() {
        }

        /**
         * Expect the result to be returned once.
         */
        public void once() {
            times(1);
        }

        /**
         * Expect the result to be never returned.
         */
        public void never() {
            times(0);
        }

        /**
         * Expect the result to be returned a given number of times.
         * 
         * @param count number of times a result should be returned
         */
        public void times(int count) {
            if (count < 0) {
                throw new IllegalArgumentException("Number of expected call count cannot be negative.");
            }

            timesInternal(count);
        }

        /**
         * Expect the result to be returned any times.
         */
        public void anyTimes() {
            timesInternal(ANY_TIMES);
        }

        /**
         * Expects the result to be returned a given number of times.
         * <p>
         * If expected result is not defined, creates a default expected <code>null</code> result.
         * 
         * @param count number of times a result should be returned
         */
        private void timesInternal(int count) {
            if (expectedResult == null) {
                buildReturnResult(null);
            }

            expectedResult.setTimesExpected(count);
        }
    }
}
